﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using log4net;
using Reactor.Environment;
using Samurai.Wakizashi;

namespace Reactor.Messaging.ServiceBus
{
    public class DefaultDestinationRegistry : DisposableBase, IDestinationRegistry
    {
        #region Fields

        private readonly PlatformConnectionData _connectionData;
        private readonly ISubscriptionDataProvider _subscriptionDataProvider;
        private static readonly ILog Log = LogManager.GetLogger(typeof (DefaultDestinationRegistry));
        private readonly object _subscriberSync = new object();
        private readonly string _connectionString;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="DefaultDestinationRegistry"/> class.
        /// </summary>
        /// <param name="connectionData">The connection data.</param>
        /// <param name="subscriptionDataProvider">The subscription data provider to use when saving or retrieving subscription records.</param>
        /// <param name="instanceSubscriber">The instance to provide the bus instance subscriptions. The instance provided in this constructor need only have a name. The MessagingChannel registry when add subscription mappings as they are discovered.</param>
        /// <exception cref="ArgumentNullException">Throws an <see cref="ArgumentNullException"/> if connectionData argument is null.</exception>
        /// <exception cref="ArgumentNullException">Throws an <see cref="ArgumentNullException"/> if subscriptionDataProvider argument in null.</exception>
        /// <remarks>This instance expects a connection string named 'ReactorServices'. If not found in the provided <seealso cref="PlatformConnectionData"/>,
        /// the instance will go into passive mode. Once in passive mode, the instance will accept subscriber registrations and will provide
        /// default destinations for all messages. Default MessagingChannel is a JMS topic having the same name as the
        /// message type. No messages will go to JMS queues unless subscribers spcifying an interest are registered.</remarks>
        public DefaultDestinationRegistry(PlatformConnectionData connectionData, ISubscriptionDataProvider subscriptionDataProvider, Subscriber instanceSubscriber)
        {
            if (connectionData == null) throw new ArgumentNullException("connectionData");
            if (subscriptionDataProvider == null) throw new ArgumentNullException("subscriptionDataProvider");
            if (instanceSubscriber == null) throw new ArgumentNullException("instanceSubscriber");

            _connectionData = connectionData;
            _subscriptionDataProvider = subscriptionDataProvider;
            InstanceSubscriber = instanceSubscriber;

            Subscribers = new ConcurrentBag<Subscriber>();

            _connectionString = _connectionData.DatabaseConnectionString;
            if (string.IsNullOrEmpty(_connectionString))
            {
                Log.Warn("Entering passive mode because no ReactorServices connection string was found.");
                EnterPassiveMode();
            }
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets a value indicating whether this instance is passive. If passive, no SQL Server connection 
        /// string named 'ReactorServices' was found. If true, this instance will accept subscriber registrations and will provide 
        /// default destinations for all messages. Default MessagingChannel is a JMS topic having the same name as the 
        /// message type. No messages will go to JMS queues unless subscribers spcifying an interest are registered.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is passive; otherwise, <c>false</c>.
        /// </value>
        public bool IsPassive { get; private set; }

        /// <summary>
        /// Gets or sets a list of all subscribers known.
        /// </summary>
        /// <value>The subscribers.</value>
        /// <remarks>Access to this collection is thread safe.</remarks>
        public ConcurrentBag<Subscriber> Subscribers { get; private set; }

        /// <summary>
        /// Gets or sets the subscriber instance for the current bus endpoint. Manipulating 
        /// the <see cref="Subscriber"/> instance directly is not a thread safe operation. To 
        /// remain thread safe, use the appropriate methods on this <see cref="IDestinationRegistry"/> 
        /// instance instead.
        /// </summary>
        /// <value>The current subscriber.</value>
        public Subscriber InstanceSubscriber { get; private set; }

        #endregion

        #region Public Methods

        /// <summary>
        /// Appends data for the current endpoint's subscriptions.
        /// </summary>
        /// <param name="messagingChannel">A MessagingChannel for the supplied message types.</param>
        /// <param name="messageTypes">Type of messages to be sent to the MessagingChannel provided.</param>
        /// <exception cref="ArgumentNullException">Throws an <see cref="ArgumentNullException"/> if the <paramref name="messagingChannel"/> parameter is null.</exception>
        /// <exception cref="ArgumentNullException">Throws an <see cref="ArgumentNullException"/> if the <paramref name="messageTypes"/> parameter is null.</exception>
        /// <exception cref="InvalidOperationException">Throws an <see cref="InvalidOperationException"/> if the <paramref name="messageTypes"/> array is empty.</exception>
        public void AppendSubscriptionData(IMessagingChannel messagingChannel, Type[] messageTypes)
        {
            if (messagingChannel == null) throw new ArgumentNullException("messagingChannel");
            if (messageTypes == null) throw new ArgumentNullException("messageTypes");
            if (messageTypes.Length == 0) throw new InvalidOperationException("No message types provided.");

            lock (_subscriberSync)
            {
                var found = false;
                foreach (var destinationMapping in InstanceSubscriber.Mappings)
                {
                    if (!destinationMapping.MessagingChannel.Equals(messagingChannel)) continue;

                    found = true;
                    destinationMapping.AddMessageTypes(messageTypes);
                    break;
                }

                // If we have no mapping for the MessagingChannel, create one
                if(!found)
                {
                    var destinationMapping = new Subscriber.ChannelMessagesMapping { MessagingChannel = messagingChannel };
                    destinationMapping.AddMessageTypes(messageTypes);
                    InstanceSubscriber.Mappings.Add(destinationMapping);
                }
            }
        }

        /// <summary>
        /// Invalidates the specified subscription.
        /// </summary>
        /// <param name="messagingChannel"></param>
        /// <param name="messageType"></param>
        public void InvalidateSubscription(IMessagingChannel messagingChannel, Type messageType)
        {
            Subscriber.ChannelMessagesMapping mapping;

            lock (_subscriberSync)
            {
                mapping = InstanceSubscriber.Mappings.SingleOrDefault(m => m.MessagingChannel.Equals(messagingChannel));

                if (mapping == null) return;

                mapping.RemoveMessageType(messageType);

                // Remove mapping if there are no more expected message types
                if (mapping.GetMessageTypes().Length == 0)
                    InstanceSubscriber.Mappings.Remove(mapping);
            }
        }

        /// <summary>
        /// Invalidates all subscriptions on non-durable destinations for the primary endpoint.
        /// <remarks>A call to this method results in the current subscriber record being updated and persisted.</remarks>
        /// </summary>
        public void InvalidateSubscriptionsOnNonDurableDestinations()
        {
            if (InstanceSubscriber == null) return;

            lock (_subscriberSync)
            {
                // Any topic is non-durable currently. When support for durable topics is added, this logic must change!
                var nonDurableDestinations = (from destinationMapping in InstanceSubscriber.Mappings
                                              where destinationMapping.MessagingChannel is PubSubChannel
                                              select destinationMapping).ToArray();

                foreach (var nonDurableDestination in nonDurableDestinations)
                    InstanceSubscriber.Mappings.Remove(nonDurableDestination);

                SaveCurrentSubscriptionData();
            }

            Log.Info("Subscriptions to non-durable destinations terminated for the primary endpoint.");
        }

        /// <summary>
        /// Registers the provided subscriber, making its subscription details available.
        /// </summary>
        /// <param name="subscriber">The subscriber.</param>
        public void RegisterSubscriber(Subscriber subscriber)
        {
            // Replace subscriber instance if there is one by the same name already
            RemoveSubscriberByName(subscriber.Name);
            if (Log.IsDebugEnabled) Log.DebugFormat("Registering subscriber '{0}'.", subscriber.Name);

            Subscribers.Add(subscriber);
        }

        /// <summary>
        /// Looks up subscriber's information by calling upon the ISubscriptionDataProvider. 
        /// Uses the subscribers name to identify it. If a subscriber record is found, the 
        /// subscriber instance is then registered with this <see cref="IDestinationRegistry"/> instance.
        /// </summary>
        /// <param name="subscriberName">Name of the subscriber.</param>
        public Subscriber LookupSubscriberByName(string subscriberName)
        {
            var result = _subscriptionDataProvider.GetSubscriberByName(subscriberName);

            if (result != null)
                RegisterSubscriber(result);

            return result;
        }

        /// <summary>
        /// Removes the specified subscriber from the registry.
        /// </summary>
        /// <param name="subscriberName">Name of the subscriber.</param>
        public void RemoveSubscriberByName(string subscriberName)
        {
            if (Log.IsDebugEnabled) Log.DebugFormat("Removing subscriber '{0}'.", subscriberName);

            // ReSharper disable RedundantAssignment
            var subscriberToRemove = Subscribers.FirstOrDefault(s => s.Name == subscriberName);
            // ReSharper restore RedundantAssignment

            Subscribers.TryTake(out subscriberToRemove);
        }

        /// <summary>
        /// Gets the intended destinations for the specified message type. If the message type implements an interface 
        /// defined in one or more <see cref="Subscriber.ChannelMessagesMapping"/>s, that MessagingChannel will be added to the 
        /// list of intended destinations as well.
        /// </summary>
        /// <exception cref="ArgumentNullException">Throws an <see cref="ArgumentNullException"/> if <paramref name="messageType"/> is null.</exception>
        /// <param name="messageType">Type of the message.</param>
        /// <returns>A list of intended destinations for the specified message type.</returns>
        public IList<IMessagingChannel> GetIntendedDestinations(Type messageType)
        {
            if (messageType == null) throw new ArgumentNullException("messageType");

            var messageInterfaces = messageType.GetInterfaces();
            var messageAncestorTypes = messageType.GetAllBaseTypes();

            var list = new HashSet<IMessagingChannel>(); // Using a HashSet to avoid duplicates
            foreach (var subscriber in Subscribers)
            {
                foreach (var destinationMapping in subscriber.Mappings)
                {
                    var messageTypes = destinationMapping.GetMessageTypes();

                    // Check for MessagingChannel for message concrete type
                    if (messageTypes.Contains(messageType))
                        list.Add(destinationMapping.MessagingChannel);

                    // Check for MessagingChannel for message interface types
                    if (messageInterfaces.Count(mi => messageTypes.Contains(mi)) > 0)
                        list.Add(destinationMapping.MessagingChannel);

                    // Check for MessagingChannel for message ancestor types
                    if (messageAncestorTypes.Count(at => messageTypes.Contains(at)) > 0)
                        list.Add(destinationMapping.MessagingChannel);
                }
            }

            // Don't allow an empty send-to list. Messages without explicit destinations should at least be 
            //  sent to a topic of the message type.
            if(list.Count == 0)
                list.Add(new PubSubChannel(messageType.FullName));

            return list.ToList();
        }

        /// <summary>
        /// Initializes this instance by looking up subscription details in the configured SQL Server database only 
        /// if not in passive mode (IsPassive = true).
        /// </summary>
        public void Initialize()
        {
            if(IsPassive) return;

            try
            {
                var results = _subscriptionDataProvider.GetAllSubscribers();

                foreach (var subscriber in results)
                    RegisterSubscriber(subscriber);
            }
            catch (Exception)
            {
                EnterPassiveMode();
            }
        }

        /// <summary>
        /// Saves the snapshot of the current endpoint's subscription data to the database.
        /// </summary>
        public void SaveCurrentSubscriptionData()
        {
            if (!IsPassive)
                _subscriptionDataProvider.SaveSubscriber(InstanceSubscriber);
            else
                Log.Info("Since IDestinationRegistry instance is in passive mode, no subscription persistance took place when SaveCurrentSubscriptionData was called.");
        }

        #endregion

        #region Private Methods

        private void EnterPassiveMode()
        {
            IsPassive = true;
        }

        #endregion

        #region Implementation of IDisposable

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        /// <filterpriority>2</filterpriority>
        protected override void Dispose(bool disposing)
        {
            // Clear subscribers
            var items = Subscribers.ToArray();
            foreach (var subscriber in items)
            {
                // ReSharper disable RedundantAssignment
                Subscriber s = subscriber;
                // ReSharper restore RedundantAssignment
                Subscribers.TryTake(out s);
            }

            base.Dispose(disposing);
        }

        #endregion
    }
}
